---
title: 启动画面
sidebar:
  order: 1
tableOfContents:
  minHeadingLevel: 2
  maxHeadingLevel: 5
i18nReady: true
---

import { Image } from 'astro:assets';
import step_1 from '@assets/learn/splashscreen/step_1.png';
import step_3 from '@assets/learn/splashscreen/step_3.png';
import { Steps, Tabs, TabItem } from '@astrojs/starlight/components';
import ShowSolution from '@components/ShowSolution.astro';
import CTA from '@fragments/cta.mdx';

在本实验中，我们将在 Tauri 应用中实现一个基本的启动画面功能。
实现方法非常简单，启动画面实际上就是在启动应用前执行一些繁重的设置任务时
创建一个新窗口来显示一些内容，然后在设置完成后关闭它。

## 先决条件

:::tip[创建一个实验应用]

如果你不是高级用户，我们**强烈建议**你使用此处提供的选项和框架来操作。这只是一个实验环境，完成后你可以随时删除该项目。

<CTA/>

- Project name: `splashscreen-lab`
- Choose which language to use for your frontend: `Typescript / Javascript`
- Choose your package manager: `pnpm`
- Choose your UI template: `Vanilla`
- Choose your UI flavor: `Typescript`

:::

## 步骤

<Steps>

1. ##### 安装依赖项并运行项目

   在开始开发任何项目之前，构建和运行初始模板非常重要，以验证你的设置是否按预期工作。

    <ShowSolution>
    ```sh frame=none
    # 确保你在正确的目录下
    cd splashscreen-lab
    # 安装依赖
    pnpm install
    # 构建并运行应用
    pnpm tauri dev
    ```
    <Image src={step_1} alt="Successful run of the created template app."/>
    </ShowSolution>

1. ##### 在 `tauri.conf.json` 中注册窗口

   添加新窗口最简单的方法是直接将它们添加到 `tauri.conf.json` 中。你也可以在启动时动态创建它们，
   但为了简单起见，我们直接注册它们。请确保你有一个标签为 `main` 窗口（创建时设置为隐藏窗口），
   以及一个标签为 `splashscreen` 的窗口（创建时设置为直接显示窗口）。
   你可以将所有其他选项保留为默认值，也可以根据个人喜好进行调整。

    <ShowSolution>
    ```json
    // src-tauri/tauri.conf.json
    {
        "windows": [
            {
                "label": "main",
                "visible": false
            },
            {
                "label": "splashscreen",
                "url": "/splashscreen"
            }
        ]
    }
    ```
    </ShowSolution>

1. ##### 创建新页面来托管你的启动画面

   Before you begin you'll need to have some content to show. How you develop new pages depend on your chosen framework,
   most have the concept of a "router" that handles page navigation which should work just like normal in Tauri, in which case
   you just create a new splashscreen page. Or as we're going to be doing here, create a new `splashscreen.html` file to host the contents.
   开始之前，你需要准备一些用于展示的内容。如何开发新页面取决于你选择的框架，大多数框架都包含“路由器”的概念，
   用于处理页面导航，其工作原理与 Tauri 中的常规操作相同。在这种情况下，你只需创建一个新的启动画面页面即可。
   或者，就像我们这里要做的那样，创建一个新的 `splashscreen.html` 文件来托管内容。

   What's important here is that you can navigate to a `/splashscreen` URL and be shown the contents you want for your splashscreen. Try running the app again after this step!
   这里重要的是，你可以导航到 `/splashscreen` 并显示你想要的启动画面内容。完成此步骤后，请尝试再次运行该应用程序！

    <ShowSolution>
    ```html
    // /splashscreen.html
    <!doctype html>
    <html lang="en">
    <head>
        <meta charset="UTF-8" />
        <link rel="stylesheet" href="/src/styles.css" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Tauri App</title>
    </head>
    <body>
        <div class="container">
            <h1>Tauri used Splash</h1>
            <div class="row">
                <h5>It was super effective!</h5>
            </div>
        </div>
    </body>
    </html>
    ```
    <Image src={step_3} alt="The splashscreen we just created."/>
    </ShowSolution>

1. ##### 开始一些设置任务

    由于启动画面通常用于隐藏繁重的设置相关任务，因此让我们假装给应用程序一些繁重的任务去做，一些在前端，一些在后端。

    为了模拟前端的繁重设置，我们将使用一个简单的 `setTimeout` 函数。
    
    在后端模拟繁重操作的最简单方法是使用 Tokio crate，这是 Tauri 在后端用来提供异步运行时的 Rust crate。虽然 Tauri 提供了运行时，但 Tauri 并没有从中重新导出各种实用程序，因此我们需要将该 crate 添加到项目中才能访问它们。这在 Rust 生态系统中是一种非常正常的做法。

    不要在异步函数中使用 `std::thread::sleep` ，它们在并发环境中协同运行而不是并行运行，这意味着如果你让线程而不是 Tokio 任务休眠，你将锁定计划在该线程上运行的所有任务，从而导致你的应用程序冻结。

    <ShowSolution>
    ```sh frame=none
    # 使用该命令到包含 `Cargo.toml` 文件的目录下
    cd src-tauri
    # 添加 Tokio crate
    cargo add tokio
    # 选择性的返回顶层目录以继续开发
    # `tauri dev` 也可以自动识别出从哪里启动
    cd ..
    ```

    ```javascript
    // src/main.ts
    // 这些语句可以复制粘贴到现有的代码下面，但不要替换整个文件！！

    // 在 TypeScript 中实现的一个 sleep 函数
    function sleep(seconds: number): Promise<void> {
        return new Promise(resolve => setTimeout(resolve, seconds * 1000));
    }

    // 设置函数
    async function setup() {
        // 模拟执行一个很重的前端设置任务
        console.log('Performing really heavy frontend setup task...')
        await sleep(3);
        console.log('Frontend setup task complete!')
        // 设置前端任务为完成
        invoke('set_complete', {task: 'frontend'})
    }

    // 实际上的 JavaScript main 函数
    window.addEventListener("DOMContentLoaded", () => {
        setup()
    });
    ```

    ```rust
    // /src-tauri/src/lib.rs
    // 导入我们需要使用的模块
    use std::sync::Mutex;
    use tauri::async_runtime::spawn;
    use tauri::{AppHandle, Manager, State};
    use tokio::time::{sleep, Duration};

    // 创建一个结构，用于跟踪前端任务完成情况
    // 设置相关任务
    struct SetupState {
        frontend_task: bool,
        backend_task: bool,
    }

    // 在 v2 移动兼容应用中我们的主要入口点
    #[cfg_attr(mobile, tauri::mobile_entry_point)]
    pub fn run() {
        // 不要在 Tauri 启动之前写代码，而是写在 setup 钩子中
        tauri::Builder::default()
            // 注册一个由 Tauri 管理的 `State` 
            // 我们需要对它拥有写访问权限，因此我们将其包裹在 `Mutex` 中
            .manage(Mutex::new(SetupState {
                frontend_task: false,
                backend_task: false,
            }))
            // 添加我们用于检查的命令
            .invoke_handler(tauri::generate_handler![greet, set_complete])
            // 使用 setup 钩子来执行设置相关任务
            // 在主循环之前运行，因此尚未创建窗口
            .setup(|app| {
                // Spawn 操作设置为一个非阻塞任务，以便在它执行的同时可以创建并运行窗口。
                spawn(setup(app.handle().clone()));
                // 钩子期望返回一个 Ok 的结果
                Ok(())
            })
            // 启动应用
            .run(tauri::generate_context!())
            .expect("error while running tauri application");
    }

    #[tauri::command]
    fn greet(name: String) -> String {
        format!("Hello {name} from Rust!")
    }

    // 一个用于设置初始化任务状态的自定义任务
    #[tauri::command]
    async fn set_complete(
        app: AppHandle,
        state: State<'_, Mutex<SetupState>>,
        task: String,
    ) -> Result<(), ()> {
        // 以只读方式锁定 `State`
        let mut state_lock = state.lock().unwrap();
        match task.as_str() {
            "frontend" => state_lock.frontend_task = true,
            "backend" => state_lock.backend_task = true,
            _ => panic!("invalid task completed!"),
        }
        // 检查两个任务是否都已完成
        if state_lock.backend_task && state_lock.frontend_task {
            // 设置都已完成，我们可以关闭启动画面并且显示 main 窗口了
            let splash_window = app.get_webview_window("splashscreen").unwrap();
            let main_window = app.get_webview_window("main").unwrap();
            splash_window.close().unwrap();
            main_window.show().unwrap();
        }
        Ok(())
    }

    // 一个异步函数，用于执行一些耗时的设置任务
    async fn setup(app: AppHandle) -> Result<(), ()> {
        // 模拟执行一些耗时的设置任务，3秒后完成
        println!("Performing really heavy backend setup task...");
        sleep(Duration::from_secs(3)).await;
        println!("Backend setup task completed!");
        // 设置后端任务为已完成
        // 可以像普通函数一样运行命令，但需要自己处理输入参数
        set_complete(
            app.clone(),
            app.state::<Mutex<SetupState>>(),
            "backend".to_string(),
        )
        .await?;
        Ok(())
    }
    ```
    </ShowSolution>

1. ##### 启动应用

   你现在应该会看到一个启动画面窗口弹出，前端和后端将各自执行耗时 3 秒的初始化任务，完成后启动画面会消失，并显示主窗口！

</Steps>

## 讨论

##### 你是否应该一个启动画面?

一般来说，使用启动画面其实意味着你承认自己的应用无法在足够短的时间内完成加载，以至于不得不依赖它。
事实上，更好的做法通常是直接打开主窗口，然后在界面的某个角落显示一个小的加载指示器（比如旋转的进度条），让用户知道后台仍在进行一些初始化任务。

然而，话说回来，使用启动画面也可以是一种风格上的选择，或者你可能有一些特殊需求，必须等待某些任务完成后才能启动应用。
在这种情况下，使用启动画面当然没有*错*，只是通常来说它并不是必需的，而且可能会让用户觉得这个应用优化得不够好。
